{
  lib,
  stdenv,
  buildEnv,
  runCommand,
  fetchurl,
  file,
  texlive,
  writeShellScript,
  writeText,
  texliveInfraOnly,
  texliveConTeXt,
  texliveSmall,
  texliveMedium,
  texliveFull,
}:

rec {

  mkTeXTest = lib.makeOverridable (
    {
      name,
      format,
      text,
      texLive ? texliveSmall,
      options ? "-interaction=errorstopmode",
      preTest ? "",
      postTest ? "",
      ...
    }@attrs:
    runCommand "texlive-test-tex-${name}"
      (
        {
          nativeBuildInputs = [ texLive ] ++ attrs.nativeBuildInputs or [ ];
          text = builtins.toFile "${name}.tex" text;
        }
        // removeAttrs attrs [
          "nativeBuildInputs"
          "text"
          "texLive"
        ]
      )
      ''
        export HOME="$(mktemp -d)"
        mkdir "$out"
        cd "$out"
        cp "$text" "$name.tex"
        ${preTest}
        $format $options "$name.tex"
        ${postTest}
      ''
  );

  tlpdbNix =
    runCommand "texlive-test-tlpdb-nix"
      {
        nixpkgsTlpdbNix = ../../tools/typesetting/tex/texlive/tlpdb.nix;
        tlpdbNix = texlive.tlpdb.nix;
      }
      ''
        mkdir -p "$out"
        diff -u "''${nixpkgsTlpdbNix}" "''${tlpdbNix}" | tee "$out/tlpdb.nix.patch"
      '';

  # test two completely different font discovery mechanisms, both of which were once broken:
  #  - lualatex uses its own luaotfload script (#220228)
  #  - xelatex uses fontconfig (#228196)
  opentype-fonts = lib.recurseIntoAttrs rec {
    lualatex = mkTeXTest {
      name = "opentype-fonts-lualatex";
      format = "lualatex";
      texLive = texliveSmall.withPackages (ps: [ ps.libertinus-fonts ]);
      text = ''
        \documentclass{article}
        \usepackage{fontspec}
        \setmainfont{Libertinus Serif}
        \begin{document}
          \LaTeX{} is great
        \end{document}
      '';
    };
    xelatex = lualatex.override {
      name = "opentype-fonts-xelatex";
      format = "xelatex";
    };
  };

  chktex =
    runCommand "texlive-test-chktex"
      {
        nativeBuildInputs = [
          (texlive.withPackages (ps: [ ps.chktex ]))
        ];
        input = builtins.toFile "chktex-sample.tex" ''
          \documentclass{article}
          \begin{document}
            \LaTeX is great
          \end{document}
        '';
      }
      ''
        # chktex is supposed to return 2 when it (successfully) finds warnings, but no errors,
        # see http://git.savannah.nongnu.org/cgit/chktex.git/commit/?id=ec0fb9b58f02a62ff0bfec98b997208e9d7a5998
        (set +e; chktex -v -nall -w1 "$input" 2>&1; [ $? = 2 ] || exit 1; set -e)  | tee "$out"
        # also check that the output does indeed contain "One warning printed"
        grep "One warning printed" "$out"
      '';

  context = mkTeXTest {
    name = "texlive-test-context";
    format = "context";
    texLive = texliveConTeXt;
    # check that the PDF has been created: we have hit cases of context
    # failing with exit status 0 due to a misconfigured texlive
    postTest = ''
      if [[ ! -f "$name".pdf ]] ; then
        echo "ConTeXt test failed: file '$name.pdf' not found"
        exit 1
      fi
    '';
    text = ''
      \starttext
      \startsection[title={ConTeXt test document}]
        This is an {\em incredibly} simple ConTeXt document.
      \stopsection
      \stoptext
    '';
  };

  dvipng = lib.recurseIntoAttrs {
    # https://github.com/NixOS/nixpkgs/issues/75605
    basic =
      runCommand "texlive-test-dvipng-basic"
        {
          nativeBuildInputs = [
            file
            texliveMedium
          ];
          input = fetchurl {
            name = "test_dvipng.tex";
            url = "http://git.savannah.nongnu.org/cgit/dvipng.git/plain/test_dvipng.tex?id=b872753590a18605260078f56cbd6f28d39dc035";
            sha256 = "1pjpf1jvwj2pv5crzdgcrzvbmn7kfmgxa39pcvskl4pa0c9hl88n";
          };
        }
        ''
          cp "$input" ./document.tex

          latex document.tex
          dvipng -T tight -strict -picky document.dvi
          for f in document*.png; do
            file "$f" | tee output
            grep PNG output
          done

          mkdir "$out"
          mv document*.png "$out"/
        '';

    # test dvipng's limited capability to render postscript specials via GS
    ghostscript =
      runCommand "texlive-test-ghostscript"
        {
          nativeBuildInputs = [
            file
            (texliveSmall.withPackages (ps: [ ps.dvipng ]))
          ];
          input = builtins.toFile "postscript-sample.tex" ''
            \documentclass{minimal}
            \begin{document}
              Ni
              \special{ps:
                newpath
                0 0 moveto
                7 7 rlineto
                0 7 moveto
                7 -7 rlineto
                stroke
                showpage
              }
            \end{document}
          '';
          gs_trap = writeShellScript "gs_trap.sh" ''
            exit 1
          '';
        }
        ''
          cp "$gs_trap" ./gs
          export PATH=$PWD:$PATH
          # check that the trap works
          gs && exit 1

          cp "$input" ./document.tex

          latex document.tex
          dvipng -T 1in,1in -strict -picky document.dvi
          for f in document*.png; do
            file "$f" | tee output
            grep PNG output
          done

          mkdir "$out"
          mv document*.png "$out"/
        '';
  };

  # https://github.com/NixOS/nixpkgs/issues/75070
  dvisvgm =
    runCommand "texlive-test-dvisvgm"
      {
        nativeBuildInputs = [
          file
          texliveMedium
        ];
        input = builtins.toFile "dvisvgm-sample.tex" ''
          \documentclass{article}
          \begin{document}
            mwe
          \end{document}
        '';
      }
      ''
        cp "$input" ./document.tex

        latex document.tex
        dvisvgm document.dvi -n -o document_dvi.svg
        cat document_dvi.svg
        file document_dvi.svg | grep SVG

        pdflatex document.tex
        dvisvgm -P document.pdf -n -o document_pdf.svg
        cat document_pdf.svg
        file document_pdf.svg | grep SVG

        mkdir "$out"
        mv document*.svg "$out"/
      '';

  texdoc =
    runCommand "texlive-test-texdoc"
      {
        nativeBuildInputs = [
          (texlive.withPackages (ps: [
            ps.luatex
            ps.texdoc
            ps.texdoc.texdoc
          ]))
        ];
      }
      ''
        texdoc --version

        texdoc --debug --list texdoc | tee "$out"
        grep texdoc.pdf "$out"
      '';

  # check that the default language is US English
  defaultLanguage = lib.recurseIntoAttrs rec {
    # language.def
    etex = mkTeXTest {
      name = "default-language-etex";
      format = "etex";
      text = ''
        \catcode`\@=11
        \ifnum\language=\lang@USenglish \message{[tests.texlive] Default language is US English.}
        \else\errmessage{[tests.texlive] Error: default language is NOT US English.}\fi
        \ifnum\language=0\message{[tests.texlive] Default language has id 0.}
        \else\errmessage{[tests.texlive] Error: default language does NOT have id 0.}\fi
        \bye
      '';
    };
    # language.dat
    latex = mkTeXTest {
      name = "default-language-latex";
      format = "latex";
      text = ''
        \makeatletter
        \ifnum\language=\l@USenglish \GenericWarning{}{[tests.texlive] Default language is US English}
        \else\GenericError{}{[tests.texlive] Error: default language is NOT US English}{}{}\fi
        \ifnum\language=0\GenericWarning{}{[tests.texlive] Default language has id 0}
        \else\GenericError{}{[tests.texlive] Error: default language does NOT have id 0}{}{}\fi
        \stop
      '';
    };
    # language.dat.lua
    luatex = etex.override {
      name = "default-language-luatex";
      format = "luatex";
    };
  };

  # check that all languages are available, including synonyms
  allLanguages =
    let
      hyphenBase = texlive.pkgs.hyphen-base;
      texLive = texliveFull;
    in
    lib.recurseIntoAttrs {
      # language.def
      etex = mkTeXTest {
        name = "all-languages-etex";
        format = "etex";
        inherit hyphenBase texLive;
        text = ''
          \catcode`\@=11
          \input kvsetkeys.sty
          \def\CheckLang#1{
            \ifcsname lang@#1\endcsname\message{[tests.texlive] Found language #1}
            \else\errmessage{[tests.texlive] Error: missing language #1}\fi
          }
          \comma@parse{@texLanguages@}\CheckLang
          \bye
        '';
        preTest = ''
          texLanguages="$(sed -n -E 's/^\\addlanguage\s*\{([^}]+)\}.*$/\1/p' < "$hyphenBase"/tex/generic/config/language.def)"
          texLanguages="''${texLanguages//$'\n'/,}"
          substituteInPlace "$name.tex" --subst-var texLanguages
        '';
      };
      # language.dat
      latex = mkTeXTest {
        name = "all-languages-latex";
        format = "latex";
        inherit hyphenBase texLive;
        text = ''
          \makeatletter
          \@for\Lang:=italian,@texLanguages@\do{
            \ifcsname l@\Lang\endcsname
              \GenericWarning{}{[tests.texlive] Found language \Lang}
            \else
              \GenericError{}{[tests.texlive] Error: missing language \Lang}{}{}
            \fi
          }
          \stop
        '';
        preTest = ''
          texLanguages="$(sed -n -E 's/^([^%= \t]+).*$/\1/p' < "$hyphenBase"/tex/generic/config/language.dat)"
          texLanguages="''${texLanguages//$'\n'/,}"
          substituteInPlace "$name.tex" --subst-var texLanguages
        '';
      };
      # language.dat.lua
      luatex = mkTeXTest {
        name = "all-languages-luatex";
        format = "luatex";
        inherit hyphenBase texLive;
        text = ''
          \directlua{
            require('luatex-hyphen.lua')
            langs = '@texLanguages@,'
            texio.write('\string\n')
            for l in langs:gmatch('([^,]+),') do
              if luatexhyphen.lookupname(l) \string~= nil then
                texio.write('[tests.texlive] Found language '..l..'.\string\n')
              else
                error('[tests.texlive] Error: missing language '..l..'.', 2)
              end
            end
          }
          \bye
        '';
        preTest = ''
          texLanguages="$(sed -n -E 's/^.*\[("|'\''')(.*)("|'\''')].*$/\2/p' < "$hyphenBase"/tex/generic/config/language.dat.lua)"
          texLanguages="''${texLanguages//$'\n'/,}"
          substituteInPlace "$name.tex" --subst-var texLanguages
        '';
      };
    };

  # test that language files are generated as expected
  hyphen-base =
    runCommand "texlive-test-hyphen-base"
      {
        hyphenBase = texlive.pkgs.hyphen-base;
        schemeFull = texliveFull;
        schemeInfraOnly = texliveInfraOnly;
      }
      ''
        mkdir -p "$out"/{scheme-infraonly,scheme-full}

        # create language files with no hyphenation patterns
        cat "$hyphenBase"/tex/generic/config/language.us >language.dat
        cat "$hyphenBase"/tex/generic/config/language.us.def >language.def
        cat "$hyphenBase"/tex/generic/config/language.us.lua >language.dat.lua

        cat >>language.dat.lua <<EOF
        }
        EOF

        cat >>language.def <<EOF
        %%% No changes may be made beyond this point.

        \uselanguage {USenglish}             %%% This MUST be the last line of the file.
        EOF

        for fname in language.{dat,def,dat.lua} ; do
          diff --ignore-matching-lines='^\(%\|--\) Generated by ' -u \
            {"$hyphenBase","$schemeFull"/share/texmf-var}/tex/generic/config/"$fname" \
            | tee "$out/scheme-full/$fname.patch"
          diff --ignore-matching-lines='^\(%\|--\) Generated by ' -u \
            {,"$schemeInfraOnly"/share/texmf-var/tex/generic/config/}"$fname" \
            | tee "$out/scheme-infraonly/$fname.patch"
        done
      '';

  # verify that the restricted mode gets enabled when
  # needed (detected by checking if it disallows --gscmd)
  repstopdf =
    runCommand "texlive-test-repstopdf"
      {
        nativeBuildInputs = [ (texlive.withPackages (ps: [ ps.epstopdf ])) ];
      }
      ''
        ! (epstopdf --gscmd echo /dev/null 2>&1 || true) | grep forbidden >/dev/null
        (repstopdf --gscmd echo /dev/null 2>&1 || true) | grep forbidden >/dev/null
        mkdir "$out"
      '';

  # verify that the restricted mode gets enabled when
  # needed (detected by checking if it disallows --gscmd)
  rpdfcrop =
    runCommand "texlive-test-rpdfcrop"
      {
        nativeBuildInputs = [ (texlive.withPackages (ps: [ ps.pdfcrop ])) ];
      }
      ''
        ! (pdfcrop --gscmd echo $(command -v pdfcrop) 2>&1 || true) | grep 'restricted mode' >/dev/null
        (rpdfcrop --gscmd echo $(command -v pdfcrop) 2>&1 || true) | grep 'restricted mode' >/dev/null
        mkdir "$out"
      '';

  # check that all binaries run successfully, in the following sense:
  # (1) run --version, -v, --help, -h successfully; or
  # (2) run --help, -h, or no argument with error code but show help text; or
  # (3) run successfully on a test.tex or similar file
  # we ignore the binaries that cannot be tested as above, and are either
  # compiled binaries or trivial shell wrappers
  binaries =
    let
      # TODO known broken binaries
      broken = [
        # do not know how to test without a valid build.lua
        "ppmcheckpdf"

        # 'Error initialising QuantumRenderer: no suitable pipeline found'
        "tlcockpit"
      ]
      ++ lib.optional stdenv.hostPlatform.isDarwin "epspdftk"; # wish shebang is a script, not a binary!

      # (1) binaries requiring -v
      shortVersion = [
        "devnag"
        "diadia"
        "pmxchords"
        "ptex2pdf"
        "simpdftex"
        "ttf2afm"
      ];
      # (1) binaries requiring --help or -h
      help = [
        "arlatex"
        "bundledoc"
        "cachepic"
        "checklistings"
        "dvipos"
        "extractres"
        "fig4latex"
        "fragmaster"
        "kpsewhere"
        "latex-git-log"
        "ltxfileinfo"
        "mendex"
        "pdflatexpicscale"
        "perltex"
        "pn2pdf"
        "psbook"
        "psnup"
        "psresize"
        "purifyeps"
        "simpdftex"
        "tex2xindy"
        "texluac"
        "texluajitc"
        "upmendex"
        "urlbst"
        "yplan"
      ];
      shortHelp = [
        "adhocfilelist"
        "authorindex"
        "bbl2bib"
        "bibdoiadd"
        "bibmradd"
        "biburl2doi"
        "bibzbladd"
        "bookshelf-listallfonts"
        "bookshelf-mkfontsel"
        "ctanupload"
        "disdvi"
        "dvibook"
        "dviconcat"
        "getmapdl"
        "latex2man"
        "listings-ext.sh"
        "pygmentex"
      ];
      # (2) binaries that return non-zero exit code even if correctly asked for help
      ignoreExitCode = [
        "authorindex"
        "bookshelf-listallfonts"
        "bookshelf-mkfontsel"
        "dvibook"
        "dviconcat"
        "dvipos"
        "extractres"
        "fig4latex"
        "fragmaster"
        "latex2man"
        "latex-git-log"
        "listings-ext.sh"
        "psbook"
        "psnup"
        "psresize"
        "purifyeps"
        "tex2xindy"
        "texluac"
        "texluajitc"
      ];
      # (2) binaries that print help on no argument, returning non-zero exit code
      noArg = [
        "a2ping"
        "bg5+latex"
        "bg5+pdflatex"
        "bg5latex"
        "bg5pdflatex"
        "cef5latex"
        "cef5pdflatex"
        "ceflatex"
        "cefpdflatex"
        "cefslatex"
        "cefspdflatex"
        "chkdvifont"
        "dvi2fax"
        "dvired"
        "dviselect"
        "dvitodvi"
        "epsffit"
        "findhyph"
        "gbklatex"
        "gbkpdflatex"
        "komkindex"
        "kpsepath"
        "listbib"
        "listings-ext"
        "mag"
        "mathspic"
        "mf2pt1"
        "mk4ht"
        "mkt1font"
        "mkgrkindex"
        "musixflx"
        "pdf2ps"
        "pdfclose"
        "pdftosrc"
        "pdfxup"
        "pedigree"
        "pfb2pfa"
        "pk2bm"
        "prepmx"
        "ps2pk"
        "psselect"
        "pstops"
        "rubibtex"
        "rubikrotation"
        "sjislatex"
        "sjispdflatex"
        "srcredact"
        "t4ht"
        "teckit_compile"
        "tex4ht"
        "texdiff"
        "texdirflatten"
        "texplate"
        "tie"
        "ttf2kotexfont"
        "ttfdump"
        "vlna"
        "vpl2ovp"
        "vpl2vpl"
        "yplan"
      ];
      # (3) binaries requiring a .tex file
      contextTest = [ "htcontext" ];
      latexTest = [
        "de-macro"
        "e2pall"
        "htlatex"
        "htxelatex"
        "makeindex"
        "pslatex"
        "rumakeindex"
        "tpic2pdftex"
        "wordcount"
        "xhlatex"
      ];
      texTest = [
        "fontinst"
        "htmex"
        "httex"
        "httexi"
        "htxetex"
      ];
      # tricky binaries or scripts that are obviously working but are hard to test
      # (e.g. because they expect user input no matter the arguments)
      # (printafm comes from ghostscript, not texlive)
      ignored = [
        # compiled binaries
        "dt2dv"
        "dv2dt"
        "dvi2tty"
        "dvidvi"
        "dvispc"
        "otp2ocp"
        "outocp"
        "pmxab"

        # GUI scripts that accept no argument or crash without a graphics server; please test manually
        "epspdftk"
        "texdoctk"
        "tlshell"
        "xasy"

        # requires Cinderella, not open source and not distributed via Nixpkgs
        "ketcindy"
      ];
      # binaries that need a combined scheme and cannot work standalone
      needScheme = [
        # pfarrei: require working kpse to find lua module
        "a5toa4"

        # show-pdf-tags: require working kpse to find lualatex and lua modules
        "show-pdf-tags"

        # bibexport: requires kpsewhich
        "bibexport"

        # crossrefware: require bibtexperllibs under TEXMFROOT
        "bbl2bib"
        "bibdoiadd"
        "bibmradd"
        "biburl2doi"
        "bibzbladd"
        "checkcites"
        "ltx2crossrefxml"

        # epstopdf: requires kpsewhich
        "epstopdf"
        "repstopdf"

        # requires kpsewhich
        "memoize-extract.pl"
        "memoize-extract.py"

        # require other texlive binaries in PATH
        "allcm"
        "allec"
        "chkweb"
        "explcheck"
        "extractbb"
        "fontinst"
        "ht*"
        "installfont-tl"
        "kanji-config-updmap-sys"
        "kanji-config-updmap-user"
        "kpse*"
        "latexfileversion"
        "mkocp"
        "mkofm"
        "mtxrunjit"
        "pdftex-quiet"
        "pslatex"
        "rumakeindex"
        "texconfig"
        "texconfig-sys"
        "texexec"
        "texlinks"
        "texmfstart"
        "typeoutfileinfo"
        "wordcount"
        "xdvi"
        "xhlatex"

        # misc luatex binaries searching for luatex in PATH
        "citeproc-lua"
        "context"
        "contextjit"
        "ctanbib"
        "digestif"
        "epspdf"
        "l3build"
        "luafindfont"
        "luaotfload-tool"
        "luatools"
        "make4ht"
        "pmxchords"
        "tex4ebook"
        "texblend"
        "texdoc"
        "texfindpkg"
        "texlogsieve"
        "xindex"

        # requires full TEXMFROOT (e.g. for config)
        "mktexfmt"
        "mktexmf"
        "mktexpk"
        "mktextfm"
        "psnup"
        "psresize"
        "pstops"
        "tlmgr"
        "updmap"
        "webquiz"

        # texlive-scripts: requires texlive.infra's TeXLive::TLUtils under TEXMFROOT
        "fmtutil"
        "fmtutil-sys"
        "fmtutil-user"

        # texlive-scripts: not used in nixpkgs, need updmap in PATH
        "updmap-sys"
        "updmap-user"
      ];

      # simple test files
      contextTestTex = writeText "context-test.tex" ''
        \starttext
          A simple test file.
        \stoptext
      '';
      latexTestTex = writeText "latex-test.tex" ''
        \documentclass{article}
        \begin{document}
          A simple test file.
        \end{document}
      '';
      texTestTex = writeText "tex-test.tex" ''
        Hello.
        \bye
      '';

      # link all binaries in single derivation
      binPackages = lib.catAttrs "out" (lib.attrValues texlive.pkgs);
      binaries = buildEnv {
        name = "texlive-binaries";
        paths = binPackages;
      };
    in
    runCommand "texlive-test-binaries"
      {
        inherit
          binaries
          contextTestTex
          latexTestTex
          texTestTex
          ;
        texliveScheme = texliveFull;
      }
      ''
        loadables="$(command -v bash)"
        loadables="''${loadables%/bin/bash}/lib/bash"
        enable -f "$loadables/realpath" realpath
        mkdir -p "$out"
        export HOME="$(mktemp -d)"
        declare -i binCount=0 ignoredCount=0 brokenCount=0 failedCount=0
        cp "$contextTestTex" context-test.tex
        cp "$latexTestTex" latex-test.tex
        cp "$texTestTex" tex-test.tex

        testBin () {
          path="$(realpath "$bin")"
          path="''${path##*/}"
          if [[ -z "$ignoreExitCode" ]] ; then
            PATH="$path" "$bin" $args >"$out/$base.log" 2>&1
            ret=$?
            if [[ $ret == 0 ]] && grep -i 'command not found' "$out/$base.log" >/dev/null ; then
              echo "command not found when running '$base''${args:+ $args}'"
              return 1
            fi
            return $ret
          else
            PATH="$path" "$bin" $args >"$out/$base.log" 2>&1
            ret=$?
            if [[ $ret == 0 ]] && grep -i 'command not found' "$out/$base.log" >/dev/null ; then
              echo "command not found when running '$base''${args:+ $args}'"
              return 1
            fi
            if ! grep -Ei '(Example:|Options:|Syntax:|Usage:|improper command|SYNOPSIS)' "$out/$base.log" >/dev/null ; then
              echo "did not find usage info when running '$base''${args:+ $args}'"
              return $ret
            fi
          fi
        }

        for bin in "$binaries"/bin/* ; do
          base="''${bin##*/}"
          args=
          ignoreExitCode=
          binCount=$((binCount + 1))

          # ignore non-executable files (such as context.lua)
          if [[ ! -x "$bin" ]] ; then
            ignoredCount=$((ignoredCount + 1))
            continue
          fi

          case "$base" in
            ${lib.concatStringsSep "|" ignored})
              ignoredCount=$((ignoredCount + 1))
              continue ;;
            ${lib.concatStringsSep "|" broken})
              brokenCount=$((brokenCount + 1))
              continue ;;
            ${lib.concatStringsSep "|" help})
              args=--help ;;
            ${lib.concatStringsSep "|" shortHelp})
              args=-h ;;
            ${lib.concatStringsSep "|" noArg})
              ;;
            ${lib.concatStringsSep "|" contextTest})
              args=context-test.tex ;;
            ${lib.concatStringsSep "|" latexTest})
              args=latex-test.tex ;;
            ${lib.concatStringsSep "|" texTest})
              args=tex-test.tex ;;
            ${lib.concatStringsSep "|" shortVersion})
              args=-v ;;
            ebong)
              touch empty
              args=empty ;;
            ht)
              args='latex latex-test.tex' ;;
            pdf2dsc)
              args='--help --help --help' ;;
            typeoutfileinfo)
              args=/dev/null ;;
            *)
              args=--version ;;
          esac

          case "$base" in
            ${lib.concatStringsSep "|" (ignoreExitCode ++ noArg)})
              ignoreExitCode=1 ;;
          esac

          case "$base" in
            ${lib.concatStringsSep "|" needScheme})
              bin="$texliveScheme/bin/$base"
              if [[ ! -f "$bin" ]] ; then
                ignoredCount=$((ignoredCount + 1))
                continue
              fi ;;
          esac

          if testBin ; then : ; else # preserve exit code
            echo "failed '$base''${args:+ $args}' (exit code: $?)"
            sed 's/^/  > /' < "$out/$base.log"
            failedCount=$((failedCount + 1))
          fi
        done

        echo "tested $binCount binaries: $ignoredCount ignored, $brokenCount broken, $failedCount failed"
        [[ $failedCount = 0 ]]
      '';

  # check that all scripts have a Nix shebang
  shebangs =
    let
      binPackages = lib.catAttrs "out" (lib.attrValues texlive.pkgs);
    in
    runCommand "texlive-test-shebangs" { } (
      ''
        echo "checking that all texlive scripts shebangs are in '$NIX_STORE'"
        declare -i scriptCount=0 invalidCount=0
      ''
      + (lib.concatMapStrings (pkg: ''
        for bin in '${pkg.outPath}'/bin/* ; do
          grep -I -q . "$bin" || continue  # ignore binary files
          [[ -x "$bin" ]] || continue # ignore non-executable files (such as context.lua)
          scriptCount=$((scriptCount + 1))
          read -r cmdline < "$bin"
          read -r interp <<< "$cmdline"
          if [[ "$interp" != "#!$NIX_STORE"/* && "$interp" != "#! $NIX_STORE"/* ]] ; then
            echo "error: non-nix shebang '$interp' in script '$bin'"
            invalidCount=$((invalidCount + 1))
          fi
        done
      '') binPackages)
      + ''
        echo "checked $scriptCount scripts, found $invalidCount non-nix shebangs"
        [[ $invalidCount -gt 0 ]] && exit 1
        mkdir -p "$out"
      ''
    );

  # verify that the precomputed licensing information in default.nix
  # does indeed match the metadata of the individual packages.
  #
  # This is part of the test suite (and not the normal evaluation) to save
  # time for "normal" evaluations. To be more in line with the other tests, this
  # also builds a derivation, even though it is essentially an eval-time assertion.
  licenses =
    let
      concatLicenses = builtins.foldl' (acc: el: if builtins.elem el acc then acc else acc ++ [ el ]);
      # converts a license to its attribute name in lib.licenses
      licenseToAttrName =
        license: builtins.head (builtins.attrNames (lib.filterAttrs (n: v: license == v) lib.licenses));
      lt = (a: b: a < b);

      savedLicenses = scheme: scheme.meta.license;
      savedLicensesAttrNames = scheme: map licenseToAttrName (savedLicenses scheme);

      correctLicenses =
        scheme:
        builtins.foldl' (
          acc: pkg: concatLicenses acc (lib.toList (pkg.meta.license or [ ]))
        ) [ ] scheme.passthru.requiredTeXPackages;
      correctLicensesAttrNames = scheme: lib.sort lt (map licenseToAttrName (correctLicenses scheme));

      hasLicenseMismatch =
        scheme:
        (lib.isDerivation scheme) && (savedLicensesAttrNames scheme) != (correctLicensesAttrNames scheme);
      incorrectSchemes = lib.filterAttrs (n: hasLicenseMismatch) (texlive.combined // texlive.schemes);
      prettyPrint = name: scheme: ''
        license info for ${name} is incorrect! Note that order is enforced.
        saved: [ ${lib.concatStringsSep " " (savedLicensesAttrNames scheme)} ]
        correct: [ ${lib.concatStringsSep " " (correctLicensesAttrNames scheme)} ]
      '';
      errorText = lib.concatStringsSep "\n\n" (lib.mapAttrsToList prettyPrint incorrectSchemes);
    in
    runCommand "texlive-test-license"
      {
        inherit errorText;
      }
      (
        if (incorrectSchemes == { }) then
          "echo everything is fine! > $out"
        else
          ''
            echo "$errorText"
            false
          ''
      );

  # verify that all fixed hashes are present
  # this is effectively an eval-time assertion, converted into a derivation for
  # ease of testing
  fixedHashes =
    let
      fods = lib.concatMap (
        p:
        lib.optional (p ? tex && lib.isDerivation p.tex) p.tex
        ++ lib.optional (p ? texdoc) p.texdoc
        ++ lib.optional (p ? texsource) p.texsource
        ++ lib.optional (p ? tlpkg) p.tlpkg
      ) (lib.attrValues texlive.pkgs);
      errorText = lib.concatMapStrings (
        p:
        lib.optionalString (
          !p ? outputHash
        ) "${p.pname}-${p.tlOutputName} does not have a fixed output hash\n"
      ) fods;
    in
    runCommand "texlive-test-fixed-hashes"
      {
        inherit errorText;
        passAsFile = [ "errorText" ];
      }
      ''
        if [[ -s "$errorTextPath" ]] ; then
          cat "$errorTextPath"
          echo Failed: some TeX Live packages do not have fixed output hashes. Please read UPGRADING.md for how to generate a new fixed-hashes.nix.
          exit 1
        else
          touch "$out"
        fi
      '';
}
